package com.cellngine;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

/**
 * Java Operations
 * 
 * This class consists of static methods and functions to assist developers with their project.
 * It should only contain generic methods and functions! Anything specific belongs in its own class.
 * 
 * @author Jozef Boer <jboer@jboer.nl>
 */
public class JO
{
	/**
	 * Generates a SHA-256 hash for the given input.
	 * 
	 * @param input
	 *            A valid <code>String</code> to hash.
	 * @return A <code>String</code> which either contains the resulting hash or is empty if no
	 *         valid <code>String</code> object was provided as the first argument.
	 */
	public static String SHA256(final String input)
	{
		try
		{
			final MessageDigest md = MessageDigest.getInstance("SHA-256");
			md.update(input.getBytes());
			
			final byte byteData[] = md.digest();
			
			// convert the byte to hex format method 1
			final StringBuffer sb = new StringBuffer();
			for (int i = 0; i < byteData.length; i++)
			{
				sb.append(Integer.toString((byteData[i] & 0xff) + 0x100, 16).substring(1));
			}
			
			// convert the byte to hex format method 2
			final StringBuffer hexString = new StringBuffer();
			for (int i = 0; i < byteData.length; i++)
			{
				final String hex = Integer.toHexString(0xff & byteData[i]);
				if (hex.length() == 1)
				{
					hexString.append('0');
				}
				hexString.append(hex);
			}
			
			return hexString.toString();
		}
		catch (final NoSuchAlgorithmException e)
		{
			return "";
		}
	}
	
	/**
	 * De- or Encrypts the <code>byte[]</code> input by XOR'ing it with the output of a SHA1 PRNG.
	 * 
	 * @param input
	 *            The message which shall be de- or encrypted.
	 * @param key
	 *            The seed for the SHA1 PRNG.
	 * @return The output of the XOR operation, returned as a <code>byte[]</code>.
	 */
	public static byte[] XORSHA1PRNG(final byte[] input, final byte[] key)
	{
		byte[] output = null;
		try
		{
			final SecureRandom prng = SecureRandom.getInstance("SHA1PRNG");
			prng.setSeed(key);
			output = new byte[input.length];
			final byte[] prngKey = new byte[input.length];
			prng.nextBytes(prngKey);
			for (int i = 0; i < input.length; i++)
			{
				output[i] = (byte) (input[i] ^ prngKey[i]);
			}
		}
		catch (NoSuchAlgorithmException e)
		{
			return new byte[0];
		}
		return output;
	}
	
	/**
	 * Converts an arbitrary <code>String</code> to an array of bytes.
	 * 
	 * @param string
	 *            The string which shall be converted.
	 * @return The String, converted to a <code>byte[]</code> array.
	 */
	public static byte[] toBytes(final String string)
	{
		final byte[] output = new byte[string.length()];
		for (int i = 0; i < output.length; i++)
		{
			output[i] = (byte) string.charAt(i);
		}
		return output;
	}
	
	/**
	 * Outputs a HashMap formatted as an XML.
	 * For example, a HashMap containing the entries "hello"->"world" and "foo"->"bar" will be
	 * converted to <code>"<hello>world</hello><foo>bar</foo>"</code>.
	 * 
	 * @param map
	 *            The <code>HashMap</code> which shall be converted.
	 * @return The converted String.
	 */
	public static String hashmapToXML(final HashMap<String, String> map)
	{
		final StringBuffer xmlString = new StringBuffer();
		for (Map.Entry<String, String> entry : map.entrySet())
		{
			xmlString.append("<");
			xmlString.append(entry.getKey());
			xmlString.append(">");
			xmlString.append(entry.getValue());
			xmlString.append("</");
			xmlString.append(entry.getKey());
			xmlString.append(">\n");
		}
		return xmlString.toString();
	}
	
	/**
	 * Concatenates two byte arrays.
	 * 
	 * @param first
	 *            The first part of the concatenated <code>byte[]</code> array.
	 * @param second
	 *            The second part of the concatenated <code>byte[]</code> array.
	 * @return The concatenated <code>byte[]</code> array.
	 */
	public static byte[] byteConcat(final byte[] first, final byte[] second)
	{
		byte[] output = new byte[first.length + second.length];
		for (int i = 0; i < output.length; i++)
		{
			if (i < first.length)
			{
				output[i] = first[i];
			}
			else
			{
				output[i] = second[i - first.length];
			}
		}
		return output;
	}
	
	/**
	 * Generates random data using a self-seeding SHA1 PRNG.
	 * 
	 * @param numBytes
	 *            The amount of random data which shall be generated, measured in bytes.
	 * @return The random data, o
	 * @throws <code>IllegalArgumentException</code> if <code>numBytes</code> is smaller than 1.
	 */
	public static byte[] generateRandomBytes(final int numBytes)
	{
		if (numBytes < 1)
		{
			throw new IllegalArgumentException();
		}
		byte[] random = new byte[numBytes];
		try
		{
			SecureRandom prng = SecureRandom.getInstance("SHA1PRNG");
			prng.nextBytes(random);
			return random;
		}
		catch (NoSuchAlgorithmException e)
		{
			Cellngine.log(e, Constants.log_type.error);
		}
		return null;
	}
	
	/**
	 * Checks the formal validity of a given port number.
	 * 
	 * @param port
	 *            The port number which shall be checked.
	 * @return Whether the specified port number is within the range of 1 - 65535.
	 */
	public static boolean isPortValid(final int port)
	{
		return port >= 1 && port <= 65535;
	}
	
	/**
	 * @return A <code>long</code> with the current time in milliseconds since January 1st, 1970.
	 */
	public static long getTimestamp()
	{
		return (System.currentTimeMillis());
	}
	
	/**
	 * Checks if a given BigDecimal is zero. If no valid BigDecimal is provided JO.toBigDecimal is
	 * called to convert it.
	 * 
	 * @param b
	 *            A <code>BigDecimal</code> that needs to be checked.
	 * @return <code>true</code> if the given parameter is zero (or not a valid number),
	 *         <code>false</code> if not.
	 */
	public static boolean isZero(BigDecimal b)
	{
		b = JO.toBigDecimal(b);
		return (b.compareTo(Constants.ZERO) == 0);
	}
	
	/* **************************************************************************************************** */

	public static BigDecimal toBigDecimal(final String v)
	{
		try
		{
			return new BigDecimal(v.replace(',', '.'));
		}
		catch (final Exception e)
		{
			return Constants.ZERO;
		}
	}
	
	public static BigDecimal toBigDecimal(final int v)
	{
		return toBigDecimal(v + ".00");
	}
	
	public static BigDecimal toBigDecimal(final Integer v)
	{
		return toBigDecimal(v + ".00");
	}
	
	public static BigDecimal toBigDecimal(final Object o)
	{
		try
		{
			return new BigDecimal(((BigDecimal) o).toString());
		}
		catch (final Exception e)
		{
			return Constants.ZERO;
		}
	}
	
	public static byte[] getBytesFromInputStream(final InputStream is)
	{
		return getBytesFromInputStream(is, true);
	}
	
	public static byte[] getBytesFromInputStream(final InputStream is, final boolean closeStream)
	{
		if (is == null)
		{
			return null;
		}
		
		final ByteArrayOutputStream os = new ByteArrayOutputStream();
		final byte[] buffer = new byte[1024];
		int n;
		
		try
		{
			while ((n = is.read(buffer, 0, buffer.length)) != -1)
			{
				os.write(buffer, 0, n);
			}
			
			os.flush();
			
			return os.toByteArray();
		}
		catch (final IOException e)
		{
			Cellngine.log(e, Constants.log_type.error);
		}
		finally
		{
			if (closeStream)
			{
				try
				{
					is.close();
				}
				catch (final IOException e)
				{
				}
			}
		}
		
		return null;
	}
	
	public static String getStringFromInputStream(final InputStream is)
	{
		return getStringFromInputStream(is, true);
	}
	
	public static String getStringFromInputStream(final InputStream is,
			final boolean closeInputStream)
	{
		if (is == null)
		{
			return null;
		}
		
		Reader reader;
		final Writer writer = new StringWriter();
		final char[] buffer = new char[1024];
		int n;
		
		try
		{
			reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
			while ((n = reader.read(buffer)) != -1)
			{
				writer.write(buffer, 0, n);
			}
		}
		catch (final IOException e)
		{
			Cellngine.log(e, Constants.log_type.error);
		}
		finally
		{
			if (closeInputStream)
			{
				try
				{
					is.close();
				}
				catch (final IOException e)
				{
				}
			}
		}
		
		return writer.toString();
	}
}